
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.*;
import javax.swing.border.*;

import java.io.*;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * This class is a GUI application which supports 
 * showing the copy status via progress meter bar
 * to perform the file copy by using two tasks under 
 * lock concurrency control.
 * 
 * @author Chen Wei
 */
public class Exercise29_14 extends JFrame {
    
    private JProgressBar jpb = new JProgressBar();
    private JButton jbtCopy = new JButton("Copy");
    private JTextField jtfFrom = new JTextField();
    private JButton jbtFrom = new JButton("Choose...");
    private JTextField jtfTo = new JTextField();
    private JButton jbtTo = new JButton("Choose...");
    
    public Exercise29_14() {
        // Start progress meter bar
        jpb.setValue(0);
        jpb.setMaximum(100);
        
        JPanel jPanel2 = new JPanel();
        jPanel2.setLayout(new BorderLayout());
        jPanel2.setBorder(new TitledBorder("From"));
        jPanel2.add(jtfFrom, BorderLayout.CENTER);
        jPanel2.add(jbtFrom, BorderLayout.EAST);
        
        JPanel jPanel3 = new JPanel();
        jPanel3.setLayout(new BorderLayout());
        jPanel3.setBorder(new TitledBorder("To"));
        jPanel3.add(jtfTo, BorderLayout.CENTER);
        jPanel3.add(jbtTo, BorderLayout.EAST);
        
        JPanel jPanel1 = new JPanel();
        jPanel1.setLayout(new GridLayout(2, 1));
        jPanel1.add(jPanel2);
        jPanel1.add(jPanel3);
        
        JPanel jPanel4 = new JPanel();
        jPanel4.add(jbtCopy);
        
        this.add(jpb, BorderLayout.NORTH);
        this.add(jPanel1, BorderLayout.CENTER);
        this.add(jPanel4, BorderLayout.SOUTH);
        
        jpb.setStringPainted(true); // Paint the percent in a string

        jbtFrom.addActionListener(new ActionListener() {
            
            @Override
            public void actionPerformed(ActionEvent ae) {
                jbtFromActionPerformed(ae);
            }
        });
        
        jbtTo.addActionListener(new ActionListener() {
            
            @Override
            public void actionPerformed(ActionEvent ae) {
                jbtToActionPerformed(ae);
            }
        });
        
        jbtCopy.addActionListener(new ActionListener() {
            
            public void actionPerformed(ActionEvent e) {
                Exercise27_14Task task = new Exercise27_14Task();
                
                task.addPropertyChangeListener(new PropertyChangeListener() {
                    
                    public void propertyChange(PropertyChangeEvent e) {
                        if ("progress".equals(e.getPropertyName())) {
                            jpb.setValue((Integer) e.getNewValue());
                        }
                    }
                });
                
                task.execute(); // Execute SwingWorker
            }
        });
    }
    
    private void jbtFromActionPerformed(ActionEvent evt) {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setDialogTitle("Choose source file");
        int result = fileChooser.showOpenDialog(null);
        
        if (result == JFileChooser.APPROVE_OPTION) {
            jtfFrom.setText(fileChooser.getSelectedFile().getAbsolutePath());
        }
    }
    
    private void jbtToActionPerformed(ActionEvent evt) {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setDialogTitle("Choose destination");
        fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
        fileChooser.setFileHidingEnabled(false);
        int result = fileChooser.showSaveDialog(null);
        
        if (result == JFileChooser.APPROVE_OPTION) {
            jtfTo.setText(fileChooser.getSelectedFile().getAbsolutePath());
        }
    }
    
    public static void main(String[] args) {
        Exercise29_14 frame = new Exercise29_14();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setTitle("Exercise29_14");
        frame.setSize(400, 180);
        frame.setLocationRelativeTo(null); // Center the frame
        frame.setVisible(true);
    }

    // Copy file and update progress bar in a separate thread
    /**
     * This inner class performs the actual copy action and updates 
     * the progress bar. 
     * 
     * Shared queue is used to allow Reader and Write to put and
     * get contents. Each element in the queue is a 1024 bytes 
     * buffer and the maximum number of elements is 20. If the
     * Reader is very fast and fills the queue with 20 elements,
     * it will wait for the Writer to get the elements from the
     * $sharedQueue. The percentage of the progress bar is calculated
     * via (bytes written to $outFile / total bytes of $inFile).
     */
    class Exercise27_14Task extends SwingWorker<Integer, Object> {
        
        private int currentValue = 0;
        File inFile = null;
        File outFile = null;

        /** Code run on a background thread */
        @Override
        protected Integer doInBackground() {
            inFile = new File(jtfFrom.getText().trim());
            
            outFile = new File(jtfTo.getText().trim());
            if (outFile.isDirectory()) {
                outFile = new File(outFile.getAbsolutePath() + "/" + inFile.getName());
            }
            
            copy();
            return 0;
        }
        
        public void copy() {
            ExecutorService executor = Executors.newCachedThreadPool();
            executor.execute(new Writer());
            executor.execute(new Reader());
            executor.shutdown();
            
            while (!executor.isTerminated()) {
            }
            System.out.println("completed!");
        }
        
        private Lock lock = new ReentrantLock();
        private Condition newReader = lock.newCondition();
        private Condition newWriter = lock.newCondition();
        private Queue<byte[]> sharedQueue = new LinkedList<byte[]>();
        private Queue<Integer> sharedNBytesQueue = new LinkedList<Integer>();
        private long totalBytes = 0;
        private long bytesWritten = 0;
        
        class Reader implements Runnable {
            
            @Override
            public void run() {
                try {
                    BufferedInputStream in = new BufferedInputStream(
                        new FileInputStream(inFile));
                    totalBytes = in.available();
                    
                    int nbytes = 0;
                    do {
                        byte[] buffer = new byte[1024];
                        nbytes = in.read(buffer, 0, buffer.length);

                        //
                        // Critical section
                        //
                        lock.lock();
                        try {
                            if (nbytes != -1) {
                                // Enlarge the shared queue to increase the concurrency.
                                if (sharedQueue.size() >= 20) {
                                    newReader.await();
                                }
                                
                                /* offer() - inserts the specified element. */
                                sharedQueue.offer(buffer);
                                sharedNBytesQueue.offer(nbytes);
                                newWriter.signalAll();
                            }
                        }
                        catch (InterruptedException ex) {
                            ex.printStackTrace();
                        }
                        finally {
                            lock.unlock();
                        }
                    } while (nbytes != -1);
                    in.close();
                    
                }
                catch (FileNotFoundException e) {
                    e.printStackTrace();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        
        class Writer implements Runnable {
            
            @Override
            public void run() {
                try {
                    BufferedOutputStream out = new BufferedOutputStream(
                        new FileOutputStream(outFile));
                    
                    int nBytes = 0;
                    boolean finished = false;
                    
                    while (!finished) {
                        //
                        // Critical section
                        //
                        // out.write(...);
                        //
                        lock.lock();
                        try {
                            if (!sharedQueue.isEmpty()) {
                                /* poll() - retrieves and removes the head 
                                 * of the queue. or null if it is empty.
                                 */
                                nBytes = sharedNBytesQueue.poll();
                                
                                out.write(sharedQueue.poll(), 0, nBytes);
                                bytesWritten += nBytes;
                                newReader.signalAll();
                                
                                currentValue = (int) (bytesWritten * 100 / totalBytes);
                                setProgress(currentValue);
                            }
                            else {
                                newWriter.await();
                            }
                            
                            /* $finished flag is set when Writer has written
                             * all bytes to $outFile. */
                            if (bytesWritten == totalBytes && totalBytes != 0) {
                                finished = true; 
                            }
                        }
                        catch (InterruptedException ex) {
                            ex.printStackTrace();
                        }
                        finally {
                            lock.unlock();
                        }
                    }
                    out.close();
                    
                }
                catch (FileNotFoundException e) {
                    e.printStackTrace();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
